package cn.jbolt.core.api;

import cn.hutool.core.util.IdUtil;
import cn.jbolt.core.base.config.JBoltConfig;
import cn.jbolt.core.cache.JBoltUserCache;
import cn.jbolt.core.common.enums.JBoltLoginState;
import cn.jbolt.core.kit.JBoltControllerKit;
import cn.jbolt.core.kit.JBoltSaasTenantKit;
import cn.jbolt.core.model.Application;
import cn.jbolt.core.model.LoginLog;
import cn.jbolt.core.model.OnlineUser;
import cn.jbolt.core.model.User;
import cn.jbolt.core.para.JBoltParaValidator;
import cn.jbolt.core.service.JBoltLoginLogUtil;
import cn.jbolt.core.service.JBoltOnlineUserService;
import cn.jbolt.core.service.JBoltUserService;
import com.jfinal.aop.Aop;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.Base64Kit;
import com.jfinal.kit.HashKit;
import com.jfinal.kit.JsonKit;
import com.jfinal.kit.StrKit;
import com.jfinal.log.Log;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.SecretKey;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JBolt平台关于API JWT的实用管理工具类
 *
 * @ClassName: JBoltApiJwtManger
 * @author: JFinal学院-小木 QQ：909854136
 * @date: 2019年9月12日
 */
public class JBoltApiJwtManger {
    private static final Log API_LOG = Log.getLog("JBoltApiLog");
    /**
     * 有效期 默认两个小时
     * 2 * 60 * 60 *1000 2小时
     */
    public static final long JWT_TTL = 7200000L;
    /**
     * refresh token 在jwt基础上的多余有效期时间 默认 7天
     * 7 * 24 * 60 * 60 *1000
     */
    public static final long REFRESH_JWT_SURPLUS_TTL = 604800000L;
    /**
     * 定义调用接口方传递TOKEN数据的KEY键
     */
    public final static String JBOLT_API_TOKEN_KEY = "jboltjwt";
    public final static String JBOLT_API_REFRESH_TOKEN_KEY = "jboltrefreshjwt";
    public final static String JBOLT_JWT_SIGNATURE = "jboltsignature";
    public final static String JBOLT_JWT_TIMESTAMP = "jbolttimestamp";
    public final static String JBOLT_JWT_NONCE = "jboltnonce";
    private static JBoltApiJwtManger me = new JBoltApiJwtManger();

    public static JBoltApiJwtManger me() {
        return me;
    }

    /**
     * 创建JBolt Api Token (JWT)
     *
     * @param jwtParseRet
     * @return
     */
    public String createJBoltApiToken(JBoltJwtParseRet jwtParseRet) {
        // 默认两个小时过期
        return createJBoltApiToken(JBoltApiKit.getApplication(), jwtParseRet.getApiUser());
    }

    /**
     * 创建JBolt Api Token (JWT)
     *
     * @param application 所属应用APP
     * @param apiUser     签发用户
     * @return
     */
    public String createJBoltApiToken(Application application, JBoltApiUser apiUser) {
        // 默认两个小时过期
        // 生成JWT 签发 时间
        long nowMillis = System.currentTimeMillis();
        return createJBoltApiToken(application, apiUser, nowMillis, JWT_TTL);
    }

    /**
     * 创建JBolt Api Token (JWT)
     *
     * @param application 所属应用APP
     * @param apiUser     签发用户
     * @param ttlMillis   过期时长
     * @return
     */
    public String createJBoltApiToken(Application application, JBoltApiUser apiUser,Long ttlMillis) {
        // 默认两个小时过期
        // 生成JWT 签发 时间
        long nowMillis = System.currentTimeMillis();
        return createJBoltApiToken(application, apiUser, nowMillis, ttlMillis);
    }

    /**
     * 创建JBolt Api Token (JWT) 带jboltrefreshjwt
     *
     * @param application                 所属应用APP
     * @param apiUser                     签发用户
     * @param ttlMillis                   jwt过期时长
     * @param refreshJwtSurplusTtlMillis  refreshJwt在jwt基础上多余过期时长
     * @return
     */
    public String[] createJBoltApiTokens(Application application, JBoltApiUser apiUser, Long ttlMillis, Long refreshJwtSurplusTtlMillis) {
        //如果开启了saas模式 需要把当前用户的所属租户设置
        if(JBoltConfig.SAAS_ENABLE && JBoltSaasTenantKit.me.isSelfRequest() && (!apiUser.getIsTenant()||StrKit.isBlank(apiUser.getTenantSn()))){
            throw new RuntimeException("saas 租户SN未设置");
        }
        // 生成JWT 签发 时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        // 计算过期时间 如果没传或者不合要求 使用默认两小时
        if (ttlMillis == null || ttlMillis <= 0) {
            ttlMillis = JWT_TTL;
        }
        if(refreshJwtSurplusTtlMillis == null || refreshJwtSurplusTtlMillis<=0){
            refreshJwtSurplusTtlMillis = REFRESH_JWT_SURPLUS_TTL;
        }
        Date ttlDate = new Date(nowMillis + ttlMillis);
        Date refreshTtlDate = new Date(nowMillis + ttlMillis + refreshJwtSurplusTtlMillis);
        // 得到加密秘钥 配置 一个app
        SecretKey key = generalKey(application.getAppSecret());
        //设置APIUser是否检测有效性
        if(JBoltConfig.IS_NEED_CHECK_JWT_API_USER_EFFECTIVE){
            if(JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN == null || JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN.length() == 0){
                throw new RuntimeException("参数配置check_jwt_api_user_effective_token不能为空");
            }
            apiUser.setCheckEffectiveToken(JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN);
        }
        // 构建claims
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("apiUser", JsonKit.toJson(apiUser));

        //生成jwt
        String jwt = Jwts.builder()
                .header()
                .add("typ", "JWT")
                .add("alg", "HS256")
                .and()
                .claims(claims)
                .id(String.format("%s_%s_%s", apiUser.getApplicationId(), apiUser.getUserId(),
                        IdUtil.fastSimpleUUID()))
                .issuer("JBOLT")
                .subject(apiUser.getUserName())
                .issuedAt(now)
                .expiration(ttlDate)
                .signWith(key,Jwts.SIG.HS256)
                .compact();
        //生成refresh jwt
        String refreshJwt = Jwts.builder()
                .header()
                .add("typ", "JWT")
                .add("alg", "HS256")
                .and()
                .claims(claims)
                .id(String.format("%s_%s_%s", apiUser.getApplicationId(), apiUser.getUserId(),
                        IdUtil.fastSimpleUUID()))
                .issuer("JBOLT")
                .subject(apiUser.getUserName())
                .issuedAt(now)
                .signWith(key,Jwts.SIG.HS256)
                .expiration(refreshTtlDate)
                .compact();

        // 构建payload 加密 合并压缩 生成 token
        return new String[]{jwt,refreshJwt};
    }

    /**
     * 创建JBolt Api Token (JWT)
     *
     * @param application 所属应用APP
     * @param apiUser     签发用户
     * @param nowMillis   当前时间
     * @param ttlMillis   过期时长
     * @return
     * @throws Exception
     */
    public String createJBoltApiToken(Application application, JBoltApiUser apiUser, Long nowMillis, Long ttlMillis) {
        //如果开启了saas模式 需要把当前用户的所属租户设置
        if(JBoltConfig.SAAS_ENABLE  && JBoltSaasTenantKit.me.isSelfRequest() && (!apiUser.getIsTenant()||StrKit.isBlank(apiUser.getTenantSn()))){
            throw new RuntimeException("saas 租户SN未设置");
        }
        Date now = new Date(nowMillis);
        // 计算过期时间 如果没传或者不合要求 使用默认两小时
        if (ttlMillis == null || ttlMillis <= 0) {
            ttlMillis = JWT_TTL;
        }
        Date ttlDate = new Date(nowMillis + ttlMillis);
        // 得到加密秘钥 配置 一个app
        SecretKey key = generalKey(application.getAppSecret());
        //设置APIUser是否检测有效性
        if(JBoltConfig.IS_NEED_CHECK_JWT_API_USER_EFFECTIVE){
            if(JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN == null || JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN.length() == 0){
                throw new RuntimeException("参数配置check_jwt_api_user_effective_token不能为空");
            }
            apiUser.setCheckEffectiveToken(JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN);
        }
        // 构建claims
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("apiUser", JsonKit.toJson(apiUser));

        // 构建payload 加密 合并压缩 生成 token
        return Jwts.builder()
                .header()
                .add("typ", "JWT")
                .add("alg", "HS256")
                .and()
                .claims(claims)
                .id(String.format("%s_%s_%s", apiUser.getApplicationId(), apiUser.getUserId(),
                        IdUtil.fastSimpleUUID()))
                .issuer("JBOLT")
                .subject(apiUser.getUserName())
                .issuedAt(now)
                .signWith(key,Jwts.SIG.HS256)
                .expiration(ttlDate)
                .compact();
    }

    /**
     * 将APP的appSecret 生成 SecretKey
     *
     * @param appSecret
     * @return
     */
    private SecretKey generalKey(String appSecret) {
        byte[] encodedKey = Base64Kit.decode(appSecret);
        return Keys.hmacShaKeyFor(encodedKey);
    }

    /**
     * 从请求header里获取JWT生成的JBOLT API TOKEN
     *
     * @return
     */
    private String getJBoltApiToken(Controller controller) {
        return controller.getHeader(JBOLT_API_TOKEN_KEY);
    }

    /**
     * 从请求header里获取JWT生成的JBOLT API REFRESH TOKEN
     *
     * @return
     */
    private String getJBoltApiRefreshToken(Controller controller) {
        return controller.getHeader(JBOLT_API_REFRESH_TOKEN_KEY);
    }

    /**
     * 从请求header里获取jboltjwt或jboltrefreshjwt
     * @param invocation
     * @param refreshJwt
     * @return
     */
    public JBoltJwtParseRet getJwtParseRet(Invocation invocation,boolean refreshJwt) {
        return getJwtParseRet(invocation.getController(),refreshJwt);
    }

    /**
     * 刷新jwt
     * @param controller
     * @return
     */
    public JBoltApiRet refreshJwt(Controller controller){
        JBoltJwtParseRet parseRet = getJwtParseRet(controller,true);
        if(parseRet.isOk()){
            JBoltApiUser apiUser = parseRet.getApiUser();
            //创建登录日志
            LoginLog log= JBoltLoginLogUtil.createLoginLog(controller.getRequest());
            log.setUsername(apiUser.getUserName());
            log.setLoginState(JBoltLoginState.REFERSH_JWT_LOGIN_SUCCESS.getValue());
            log.save();
            User user = JBoltUserCache.me.get(apiUser.getUserIdToLong());
            if(user == null){
                return JBoltApiRet.API_FAIL("重新刷新授权失败，登录日志保存异常");
            }
            JBoltUserService userService = Aop.get(JBoltUserService.class);
            JBoltOnlineUserService onlineUserService = Aop.get(JBoltOnlineUserService.class);
            //处理用户登录信息 异地登录异常信息
            boolean isRemoteLogin= userService.processUserRemoteLogin(user,log);
            userService.processUserLoginInfo(user,isRemoteLogin,log);
            OnlineUser onlineUser = onlineUserService.processUserLogin(user,log);
            if(onlineUser == null){
                return JBoltApiRet.API_FAIL("在线用户生成异常 无法成功授权");
            }
            JBoltApiUserBean apiUserBean = new JBoltApiUserBean(apiUser.getApplicationId(),user.getId(), user.getUsername(),apiUser.getIsSystemUser(),onlineUser.getId());
            //如果存在绑定user 就获取后重新赋值
            if(JBoltParaValidator.isOk(apiUser.getBindUsers())){
                apiUserBean.setBindUsers(apiUser.getBindUsers());
            }
            //用户存在的话 设置到threadLocal中
            JBoltApiKit.setApplyJwtUser(apiUserBean);
            return JBoltApiRet.success();
        }
        return parseRet.getApiRet();
    }


    /**
     * 从请求header里获取jboltjwt或者jboltrefreshjwt
     * @param controller
     * @param refreshJwt
     * @return
     */
    public JBoltJwtParseRet getJwtParseRet(Controller controller,boolean refreshJwt) {
        String token = refreshJwt?getJBoltApiRefreshToken(controller):getJBoltApiToken(controller);
        if (StrKit.isBlank(token)) {
            return new JBoltJwtParseRet().setApiRet(refreshJwt?JBoltApiRet.NO_JBOLT_REFRESH_JWT:JBoltApiRet.NO_JBOLT_JWT);
        }
        // 校验application
        String appId = JBoltApiKit.getAppId();
        if (StrKit.isBlank(appId)) {
            return new JBoltJwtParseRet().setApiRet(JBoltApiRet.NO_JBOLT_APPID);
        }
        Application application = JBoltApiKit.getApplication();
        if (application == null) {
            return new JBoltJwtParseRet().setApiRet(JBoltApiRet.APPLICATION_NOT_EXIST(appId));
        }
        if (!application.getEnable()) {
            return new JBoltJwtParseRet().setApiRet(JBoltApiRet.APPLICATION_NOT_ENABLE(application));
        }
        // 判断是否需要校验sign
        if (application.getNeedCheckSign()) {
            boolean checkSignSuccess = checkJBoltJwtSignature(controller, token);

            if (!checkSignSuccess) {
                return new JBoltJwtParseRet().setSignCheckFailed(true)
                        .setApiRet(JBoltApiRet.SIGNATURE_CHECK_FAILED(application));
            }
        }
        JBoltJwtParseRet parseRet = parseJWT(application, token,refreshJwt);
        if(parseRet.isExpired()){
            //过期后处理对应onlineUser
            processOnlineUserDelete(token,refreshJwt);
        }
        return parseRet;
    }

    private void processOnlineUserDelete(String token, boolean refreshJwt) {
        JBoltOnlineUserService onlineUserService = Aop.get(JBoltOnlineUserService.class);
        onlineUserService.deleteByJwt(token,refreshJwt);
    }

    /**
     * 验证Signature
     *
     * @param signature
     * @param token
     * @param timestamp
     * @param nonce
     * @return
     */
    private boolean checkSignature(String signature, String token, String timestamp, String nonce) {
        String array[] = {token, timestamp, nonce};
        Arrays.sort(array);
        String tempStr = array[0] + array[1] + array[2];
        tempStr = HashKit.sha1(tempStr);
        return tempStr.equalsIgnoreCase(signature);
    }

    /**
     * 验证Jbolt_jwt_sign
     *
     * @param controller
     * @param token
     * @return
     */
    private boolean checkJBoltJwtSignature(Controller controller, String token) {
        String signature = controller.getHeader(JBOLT_JWT_SIGNATURE);
        String timestamp = controller.getHeader(JBOLT_JWT_TIMESTAMP);
        String nonce = controller.getHeader(JBOLT_JWT_NONCE);
        if (StrKit.isBlank(signature) || StrKit.isBlank(timestamp) || StrKit.isBlank(nonce)) {
            API_LOG.error("check signature failure: params are empty");
            return false;
        }

        if (checkSignature(signature, token, timestamp, nonce)) {
            return true;
        }
        API_LOG.error("check signature failure: " + " signature = " + signature + " timestamp = "
                + timestamp + " nonce = " + nonce);

        return false;
    }

    /**
     * 解析jwt
     *
     * @param application
     * @param jwt
     * @param refreshJwt
     * @return
     */
    public JBoltJwtParseRet parseJWT(Application application, String jwt,boolean refreshJwt) {
        JBoltJwtParseRet jwtParseResut = new JBoltJwtParseRet();
        if (StrKit.isBlank(jwt)) {
            return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(4, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"参数为空"));
        }
        // 验证jwt三段格式
        String[] jwtArray = jwt.split("\\.");
        if (jwtArray == null || jwtArray.length != 3) {
            return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(5, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"格式不正确"));
        }
        // 签名秘钥和生成的签名的秘钥一模一样
        SecretKey secretKey = generalKey(application.getAppSecret());
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .verifyWith(secretKey)
                    .build()
                    .parseSignedClaims(jwt)
                    .getPayload();
            if (claims == null || !claims.containsKey("apiUser")) {
                jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(1, application));
                return jwtParseResut;
            }
        } catch (MalformedJwtException | SignatureException e) {
            // 签名错误或解析错误
            jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(2, application));
            return jwtParseResut;
        } catch (ExpiredJwtException e) {
            jwtParseResut.setExpired(true);
            // 过期
            jwtParseResut.setApiRet(refreshJwt?JBoltApiRet.REFRESH_JWT_IS_EXPIRED:JBoltApiRet.JWT_IS_EXPIRED);
            return jwtParseResut;
        } catch (Throwable ex) {
            // 其它
            String msg = ex.getMessage();
            jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(3, application, msg));
            return jwtParseResut;
        }
        String apiUserJson = claims.get("apiUser", String.class);
        if (StrKit.isBlank(apiUserJson)) {
            return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(5, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"内未携带ApiUser信息"));
        }
        JBoltApiUserBean apiUser = JsonKit.parse(apiUserJson, JBoltApiUserBean.class);
        if (apiUser == null) {
            return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(6, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"携带ApiUser信息无效"));
        }
        //处理解析后判断租户信息 sn是否一致
        if(JBoltConfig.SAAS_ENABLE && JBoltSaasTenantKit.me.isSelfRequest()){
            if(!apiUser.getIsTenant() || StrKit.isBlank(apiUser.getTenantSn())){
                return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(7, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"携带ApiUser信息无效,未携带saas租户信息"));
            }
            String tenantSn = JBoltSaasTenantKit.me.getSn();
            if(!tenantSn.equals(apiUser.getTenantSn())){
                return jwtParseResut.setApiRet(JBoltApiRet.JWT_PARSE_FAIL(8, application, (refreshJwt?"jboltrefreshjwt":"jwt")+"携带ApiUser信息无效,指定saas租户信息与可访问saas租户信息不一致,不能访问当前租户数据"));
            }
        }

        //APIUser检测有效性
        if(JBoltConfig.IS_NEED_CHECK_JWT_API_USER_EFFECTIVE){
            if(JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN == null || JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN.length() == 0){
                throw new RuntimeException("参数配置check_jwt_api_user_effective_token不能为空");
            }
            //拿到解析后用户的checkToken
            String checkToken = apiUser.getCheckEffectiveToken();
            //如果未携带 说明这个无效了 需要提醒前端清理前端缓存的jwt和refreshjwt 后重新登录
            if(checkToken == null || apiUser.getCheckEffectiveToken().length() == 0){
                return jwtParseResut.setApiRet(JBoltApiRet.JWT_API_USER_CHECK_EFFECTIVE_TOKEN_FAILD);
            }
            //如果携带与服务端不同 说明服务端紧急情况修改了校验token 同样提示前端无效 需要重新登录
            if(!JBoltConfig.CHECK_JWT_API_USER_EFFECTIVE_TOKEN.equals(checkToken)){
                return jwtParseResut.setApiRet(JBoltApiRet.JWT_API_USER_CHECK_EFFECTIVE_TOKEN_FAILD);
            }
        }

        jwtParseResut.setApiUser(apiUser);
        return jwtParseResut;
    }

    /**
     * 签发JWT
     *
     * @param controller
     * @param application
     * @param jboltApplyJWT
     */
    public void createJBoltApiTokenToResponse(Controller controller, Application application,
                                              JBoltApplyJWT jboltApplyJWT) {
        JBoltApiUser apiUser = JBoltApiKit.getApplyJwtUser();
        if (apiUser != null) {
            // 创建jwt
            String[] jwts = createJBoltApiTokens(application, apiUser, jboltApplyJWT.value(),jboltApplyJWT.refreshSurplusTTL());
            processOnlineUserJwts(jwts);
            JBoltControllerKit.setApiJwts(controller, jwts[0], jwts[1],jboltApplyJWT.inHeader());
        }
    }

    private void processOnlineUserJwts(String[] jwts) {
        Long onlineUserId = JBoltApiKit.getApplyJwtOnlineUserId();
        if(onlineUserId!=null && onlineUserId>0){
            JBoltOnlineUserService onlineUserService = Aop.get(JBoltOnlineUserService.class);
            //更新onlineUser的jwts
            onlineUserService.updateOnlineUserJwts(onlineUserId,jwts[0],jwts[1]);
        }
    }

    /**
     * 重新签发JWT
     *
     * @param controller
     * @param application
     * @param jboltReApplyJWT
     */
    public void createJBoltApiTokenToResponse(Controller controller, Application application,
                                              JBoltReApplyJWT jboltReApplyJWT) {
        JBoltApiUser apiUser = JBoltApiKit.getApplyJwtUser();
        if (apiUser != null) {
            // 创建jwt
            String[] jwts = createJBoltApiTokens(application, apiUser, jboltReApplyJWT.value(),jboltReApplyJWT.refreshSurplusTTL());
            processOnlineUserJwts(jwts);
            JBoltControllerKit.setApiJwts(controller, jwts[0], jwts[1],jboltReApplyJWT.inHeader());
        }
    }
}
